package com.datang.litejvm.cmd;

import com.datang.litejvm.clz.Class;
import com.datang.litejvm.clz.Method;
import com.datang.litejvm.constant.ConstantClassInfo;
import com.datang.litejvm.constant.ConstantMethodRefInfo;
import com.datang.litejvm.constant.ConstantPool;
import com.datang.litejvm.engin.ExecutionResult;
import com.datang.litejvm.engin.JavaObject;
import com.datang.litejvm.engin.MethodArea;
import com.datang.litejvm.engin.StackFrame;

public class InvokeVirtualCmd extends TwoOperandCmd {
    public InvokeVirtualCmd(ConstantPool pool, String opCode) {
        super(pool, opCode);
    }

    @Override
    public String toString() {
        return super.toString();
    }

    /**
     * @author: 顶风少年
     * @Description: 调用实例方法，依据实例的类型进行分派
     * 无符号数 indexbyte1 和 indexbyte2 用于构建一个当前类（§2.6）的运
     * 行时常量池的索引值，构建方式为（indexbyte1 << 8）| indexbyte2，
     * 该索引所指向的运行时常量池项应当是一个方法（§5.1）的符号引用，它包
     * 含了方法的名称和描述符，以及包含该方法的接口的符号引用。这个方法的符
     * 号引用是已被解析过的（§5.4.3.3），而且这个方法不能是实例初始化方法
     * （§2.9）和类或接口的初始化方法（§2.9）。最后，如果调用的方法是
     * protected 的（§4.6），并且这个方法是当前类的父类成员，并且这个方法
     * 没有在同一个运行时包（§5.3）中定义过，那 objectref 所指向的对象的
     * 类型必须为当前类或者当前类的子类。
     * 假设 C 是 objectref 所对应的类，虚拟机将按下面规则查找实际执行的方法：
     *  如果 C 中定义了一个实例方法 M，它了重写（override，§5.4.3.5）
     * 了符号引用中表示的方法，那方法 M 就会被调用，查找过程终止。
     *  另外，如果 C 有父类，查找过程将按第一点的方式顺序递归搜索 C 的直接
     * 父类，如果超类中能搜索到符合的方法，那这个方法就会被调用。  否则，抛出 AbstractMethodError 异常。
     * 在操作数栈中，objectref 之后必须跟随 N 个参数，它们的数量、类型和顺
     * 序都必须与方法描述符所描述的保持一致。
     * 如果要调用的是同步方法，那与 objectref 相关的管程（monitor）将会进
     * 入或者重入，就如当前线程中同执行了 monitorenter 指令一般。
     * 如果要调用的不是本地方法，n 个 args 参数和 objectref 将从操作数栈中
     * 出栈。方法调用的时候，一个新的栈帧将在 Java 虚拟机栈中被创建出来，
     * objectref 和连续的 n 个参数将存放到新栈帧的局部变量表中，objectref
     * 存为局部变量 0，arg1 存为局部变量 1（如果 arg1 是 long 或 double 类型，
     * 那将占用局部变量 1 和 2 两个位置），依此类推。参数中的浮点类型数据在存
     * 入局部变量之前会先进行数值集合转换（§2.8.3）。新栈帧创建后就成为当
     * 前栈帧，Java 虚拟机的 PC 寄存器被设置成指向调用方法的首条指令，程序就
     * 从这里开始继续执行。
     * 如果要调用的是本地方法，要是这些平台相关的代码尚未绑定（§5.6）到虚
     * 拟机中的话，绑定动作先要完成。指令执行时，n 个 args 参数和 objectref
     * 将从操作数栈中出栈并作为参数传递给实现此方法的代码。参数中的浮点类型
     * 数据在传递给调用方法之前会先进行数值集合转换（§2.8.3）。参数传递和
     * 代码执行都会以具体虚拟机实现相关的方式进行。当这些平台相关的代码返回
     * 时：
     *  如果这个本地方法是同步方法，那与 objectref 相关的管程状态将会被
     * 更新，也可能退出了，就如当前线程中同执行了 monitorexit 指令一般。
     *  如果这个本地方法有返回值，那平台相关的代码返回的数据必须通过某种
     * 实现相关的方式转换成本地方法所定义的 Java 类型，并压入到操作数栈
     * 中
     * @date: 14:41 2022/6/17
     **/
    @Override
    public void execute(StackFrame frame, ExecutionResult result) {
        ConstantMethodRefInfo methodRefInfo = (ConstantMethodRefInfo) getConstantInfo(getIndex());
        String className = methodRefInfo.getClassName();
        String methodName = methodRefInfo.getMethodName();
        // 我们没有实现System.out.println方法，  所以也不用建立新的栈帧， 直接调用Java的方法， 打印出来即可。
        if (isSystemOutPrintlnMethod(className, methodName)) {
            JavaObject javaObject = (JavaObject) frame.getOperandStack().pop();
            String value = javaObject.toString();
            System.err.println("-------------------" + value + "----------------");

            // 这里就是那个out对象， 因为是个假的，直接pop出来
            frame.getOperandStack().pop();
            return;
        }

        //注意：多态， 这才是真正的对象, 先从该对象的class 中去找对应的方法，找不到的话再去找父类的方法
        JavaObject javaObject = frame.getOperandStack().peek();
        MethodArea methodArea = MethodArea.getInstance();
        Method exeMethod = null;
        String currentClassName = javaObject.getClassName();
        while (currentClassName != null) {
            Class currentClass = methodArea.findClass(currentClassName);
            exeMethod = currentClass.getMethod(methodRefInfo.getMethodName(),
                    methodRefInfo.getParamAndResult());
            if (exeMethod != null) {
                break;
            } else {
                //查找父类
                currentClassName = currentClass.getSuperClassName();
            }
        }

        if (exeMethod == null) {
            throw new RuntimeException("Can't find method for :" + methodRefInfo.toString());
        }

        //从方法区先获取下一个方法所属的类有没有加载,然后再获取对应方法.设置到到result
        result.setNextAction(ExecutionResult.PAUSE_AND_RUN_NEW_FRAME);
        result.setNextMethod(exeMethod);
    }

    private boolean isSystemOutPrintlnMethod(String className, String methodName) {
        return "java/io/PrintStream".equals(className)
                && "println".equals(methodName);
    }

}
